import argparse
import subprocess
import sys
import os
from enum import Enum
import threading
try :
    import threadpool
except ImportError:
    print("Trying to Install required module: threadpool")
    print("Usage: python -m pip install threadpool")
    sys.exit(-1)

class d8Mode(Enum):
    DEFAULT=0
    PBC_RUN=1
    BLOCKSCOPE_RUN=2

def getFileName(path, directory_exclude=None):
    f_list=os.listdir(path)
    file_arr=[]
    for root, dirs, files in os.walk(path):
        dirs[:] = [d for d in dirs if (directory_exclude == None) or (d not in directory_exclude)]
        for file in files:
            if file.endswith('.js'):
                file_arr.append(os.path.relpath(os.path.join(root, file), path))
    return file_arr

class Runner:
    def __init__(self):
        self.silent=False
        self.verbose=""
        self.d8_mode=d8Mode.DEFAULT
        self.arch="x64"
        self.mutex=threading.Lock()

    def run_command(self, command):
        if not self.silent:
            print(command)
        return subprocess.run(command, shell=True).returncode

    def command_output(self, command, executable_format=None):
        if not self.silent:
            print(command)
        return subprocess.run(command, executable=executable_format, shell=True, capture_output=True, text=True)

    def run_d8(self, file):
        print('--- Run test start: ' + file)
        d8 = os.path.dirname(os.path.realpath(__file__)) + '/out/'+self.arch+'.'+ mode + dcheck +'/d8 --expose-gc ' + extra_flags + ' '
        command={};
        temp_name = file[:-3] + '.jsc'
        if self.d8_mode == d8Mode.DEFAULT:
            command = d8 + file
        elif self.d8_mode == d8Mode.PBC_RUN:
            command = d8 + '--use-pbc --stress-maglev ' + file + self.verbose
        elif self.d8_mode == d8Mode.BLOCKSCOPE_RUN:
            command = d8 + '--use-pbc --block-scope ' + file + self.verbose
        else:
            return 1
        real = file[:-3] + '-real.txt'
        result = file[:-3] + '-result.txt'
        ret = self.run_command(command + ' > ' + real)
        if ret != 0:
          print('--- Run test ERROR: ' + command)
          return 1
        if os.path.exists(result):
          count = self.command_output(f'wc -l < {result}').stdout.strip()
          ret = self.command_output(f'diff <(tail -n {count} {real}) <(tail -n {count} {result})', '/bin/bash').returncode
          if (ret != 0):
            print('--- Run test ERROR: ' + command)
            return 1
        print('--- Run test   end: ' + file + ' SUCCESS ---')
        return 0
    def test(self, file):
        if not os.path.isfile(file):
            print(file + ' is not a valid file path.')
            exit(-1)
        return self.run_d8(file)==0

    def single_thread_run_impl(self, real_path, error_list):
        if self.test(real_path) == False:
            self.mutex.acquire()
            error_list.append(real_path)
            self.mutex.release()

    def single_thread_run(self, tests_path, files):
        error_list=[]
        for file in files:
            real_path=tests_path+file
            if self.test(real_path) == False:
                error_list.append(real_path)
        return error_list

    def multi_thread_run(self, tests_path, files, job):
        error_list=[]
        function_var=[]
        for file in files:
            function_var.append(((tests_path+file, error_list), None))
        pool=threadpool.ThreadPool(job)
        requests=threadpool.makeRequests(self.single_thread_run_impl, function_var)
        [pool.putRequest(req) for req in requests]
        pool.wait()
        return error_list

    def run(self):
        global mode
        mode="release"
        global dcheck
        dcheck=""
        global extra_flags
        extra_flags = ""
        # 创建解析器对象
        parser = argparse.ArgumentParser(description="Run v8 test. ")

        # 添加命令行参数
        parser.add_argument('--silent', action='store_true', help='Disable output at run and compile time')
        parser.add_argument('--code-cache', action='store_true', help='Run d8 command by using code cache')
        parser.add_argument('--code-cache-with-src', action='store_true', help='Run d8 command by using code cache & source file')
        parser.add_argument('--enable-precompile-hbc', action='store_true', help='Run d8 command by using hbc precompile')
        parser.add_argument('--enable-precompile-jsbc', action='store_true', help='Run d8 command by using jsbc precompile')
        parser.add_argument('--enable-run-jsbc', action='store_true', help='Run d8 command by using jsbc')
        parser.add_argument('--enable-ir', action='store_true', help='Run d8 command by using ir precompile')
        parser.add_argument('--enable-run-ir', action='store_true', help='Run d8 command by using ir')
        parser.add_argument('--enable-run-backend', action='store_true', help='Run d8 command by using new pbc backend')
        parser.add_argument('--enable-block-scope', action='store_true', help='Run d8 command by using new pbc backend && block scope')
        parser.add_argument('--enable-run-pbc', action='store_true', help='Run d8 command by using pbc')
        parser.add_argument('--hermes', action='store_true', help='Run hermes command')
        parser.add_argument('-i', '--input', type=str, nargs='+', 
                            help='Input js file. If not set, then test all js files in hermes-tests')
        parser.add_argument('-d', '--directory', type=str, default='hermes-tests',
                            help='Input js file directory. If not set, then test all js files in hermes-tests')
        parser.add_argument('-dx', '--directory-exclude', type=str, nargs='+',
                            help='It is use only if you set -d(--directory). You can specify the directories to be excluded using -d')
        parser.add_argument('--result', type=str, help='Expected result of js output. Only use it with --input.')
        parser.add_argument('--test', action='store_true', help='Same as using command --enable-precompile')
        parser.add_argument('--arch', type=str, help='Arch list example: (x64|arm|...). Default arch is x64.')
        parser.add_argument('-j', '--job', type=int, default=12, help='Number of threads. The default value is 12.')
        parser.add_argument('-m', '--mode', type=str, help='d8 mode: (release|mini|debug). The default value is release.')
        parser.add_argument('--dcheck', action='store_true', help='If mode is release, use out/arch.release-check/d8 which open the dcheck')
        parser.add_argument("--extra-flags", type=str, default="", help="extra flags pass to d8")

        # 解析命令行参数
        args = parser.parse_args()

        if args.mode!=None:
            mode=args.mode

        if mode == "release" and args.dcheck:
            dcheck="-check"
        extra_flags = args.extra_flags

        if args.test:
            args.enable_precompile=True

        if args.silent:
            self.silent=True
            self.verbose=' >/dev/null 2>&1 '

        if args.enable_run_pbc:
            self.d8_mode=d8Mode.PBC_RUN
        elif args.enable_block_scope:
            self.d8_mode=d8Mode.BLOCKSCOPE_RUN

        if args.arch!=None:
            self.arch=args.arch

        tests_path=None
        if args.input!=None:
            files=args.input
            tests_path=''
        else:
            tests_path=args.directory
            files=getFileName(tests_path, args.directory_exclude)
            tests_path=tests_path+'/'

        for file in files:
            if file[-3:] != '.js':
                print(file + ' it not valid file name')
                exit(-1)

        files_len=len(files)
        if files_len==1:
            if self.test(args.input[0]):
                print('Test ok.')
                return
            print('Test error!')
            sys.exit(-1)

        job=min(args.job, 32)
        job=min(job, files_len)
        if files_len <= 16 or job == 1:
            error_list=self.single_thread_run(tests_path, files)
        else:
            error_list=self.multi_thread_run(tests_path, files, job)

        all_cases = []
        for case in files:
            all_cases.append(tests_path + case)
        success_list = 'success_list.txt'
        with open(success_list, "w+") as f:
            f.seek(0)
            f.truncate()
            for case in all_cases:
                if case not in error_list:
                    f.write(case +'\n')
        os.system("sort -i " + success_list + " -o " + success_list);

        if not error_list:
            print('All Test Case PASS ')
            for file in files:
                file_name = tests_path+file[0:-3]
                self.run_command('rm -f ' + file_name + '.jsc;' + 
                                 'rm ' + file_name + '-real.txt;' + 
                                 'rm -f ' + file_name + '.hbc')
            return

        print('Test error list:')
        print('\n'.join(error_list))
        sys.exit(-1)
def main():
    runner=Runner()
    runner.run()

if __name__ == "__main__":
    main()
